﻿using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;

namespace Peter.Logic.AvalonEdit
{
   /// <summary>
   /// Highlights all occurrences of the currently selected word.
   /// </summary>
   public class HighlightCurrentWordBackgroundRenderer : IBackgroundRenderer
   {
      private MatchCollection m_Matches;
      private readonly TextEditor m_Editor;

      /// <summary>
      /// Initializes a new Highlight Current Line Background Renderer
      /// </summary>
      /// <param name="editor">The editor to attach to.</param>
      public HighlightCurrentWordBackgroundRenderer (TextEditor editor)
      {
         this.m_Editor = editor;
         this.m_Editor.TextArea.Caret.PositionChanged += this.OnChange;
         this.m_Editor.TextArea.SelectionChanged += this.OnChange;
         this.Add ();
      }

      /// <summary>
      /// Gets the layer to draw this on.
      /// </summary>
      public KnownLayer Layer
      {
         get { return KnownLayer.Selection; }
      }

      /// <summary>
      /// Adds this background renderer to the stack.
      /// </summary>
      public void Add ()
      {
         if (!this.m_Editor.TextArea.TextView.BackgroundRenderers.Contains (this))
            this.m_Editor.TextArea.TextView.BackgroundRenderers.Add (this);
      }

      /// <summary>
      /// Removes this background renderer from the stack.
      /// </summary>
      public void Remove ()
      {
         if (this.m_Editor.TextArea.TextView.BackgroundRenderers.Contains (this))
            this.m_Editor.TextArea.TextView.BackgroundRenderers.Remove (this);
      }

      /// <summary>
      /// Draws the line highlight
      /// </summary>
      /// <param name="textView">The text view with the line highlighted.</param>
      /// <param name="drawingContext"></param>
      public void Draw (TextView textView, DrawingContext drawingContext)
      {
         if (this.m_Matches != null && this.m_Matches.Count > 0) {
            var offset = this.m_Editor.CaretOffset;
            textView.EnsureVisualLines ();
            foreach (Match match in this.m_Matches) {
               if (offset >= match.Index && offset <= match.Index + match.Length)
                  continue;
               HighlightWord (textView, drawingContext, match.Index, match.Length);
            }
         }
      }

      /// <summary>
      /// Occurs when the caret or selection changes.
      /// </summary>
      /// <param name="sender">The object changed.</param>
      /// <param name="e">Event Arguments</param>
      private async void OnChange (object sender, EventArgs e)
      {
         await this.FindWords (this.m_Editor.Text, this.m_Editor.CaretOffset, this.m_Editor.SelectedText);
      }

      /// <summary>
      /// Task used to find the same word.
      /// </summary>
      /// <param name="text">The current document text.</param>
      /// <param name="currentOffset">The current caret offset.</param>
      /// <param name="selection">The current selection.</param>
      /// <returns>Void</returns>
      private async Task FindWords (string text, int currentOffset, string selection)
      {
         
         var word = string.Empty;
         var textLength = text.Length;
         if (currentOffset == textLength || IsWordBreak (text[currentOffset]))
            --currentOffset;
         var offset = currentOffset;
         if (!string.IsNullOrEmpty (selection))
         {
            word = selection;
         }
         else {
            while (offset >= 0 && offset < textLength && !IsWordBreak (text[offset]))
            {
               word = text[offset] + word;
               --offset;
            }
            offset = currentOffset + 1;
            while (offset >= 0 && offset < textLength && !IsWordBreak (text[offset]))
            {
               word += text[offset];
               ++offset;
            }
         }
         var length = word.Length;
         if (length <= 0 || string.IsNullOrWhiteSpace (word)) {
            this.m_Matches = null;
            return;
         }
         this.m_Matches = await Task.Run(() => Regex.Matches (text, Regex.Escape (word), RegexOptions.IgnoreCase));
      }

      /// <summary>
      /// Checks if the given character is a break in a word.
      /// </summary>
      /// <param name="character">The character to check.</param>
      /// <returns>True if character represents a break in a word.</returns>
      private static bool IsWordBreak (char character)
      {
         return !char.IsLetterOrDigit(character) && character != '_';
      }

      /// <summary>
      /// Highlights the character at the given offset.
      /// </summary>
      /// <param name="textView">Text view to highlight character at.</param>
      /// <param name="drawingContext">Drawing context used to highlight.</param>
      /// <param name="offset">Offset at which to highlight.</param>
      /// <param name="length">The length to highlight.</param>
      private static void HighlightWord (TextView textView, DrawingContext drawingContext, int offset, int length)
      {
         var segment = new TextSegment {StartOffset = offset, EndOffset = offset + length};
         foreach (var r in BackgroundGeometryBuilder.GetRectsForSegment (textView, segment))
            drawingContext.DrawRectangle (Brushes.Gainsboro, null, new Rect (r.TopLeft, r.Size));
      }
   }
}
